1.线程和进程的概念、并行和并发的概念

  • 进程对应操作系统的运行模块,一个进程可以由多个线程具体实现。对应的是和宏观和微观上的不同。
  • 线程的 5 种状态:新建、就绪、运行、堵塞、死亡

2.创建线程的方式及实现

  • new thread

  • implement runable 接口

  • callable :又返回值,可以抛异常。

    public class Test2 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            FutureTask futureTask = new FutureTask(new callDemo());
            new Thread(futureTask).start();
            System.out.println(futureTask.get().toString());
        }
    }
    class callDemo implements Callable {
        @Override
        public Object call() throws Exception {
            Thread.sleep(5000);
            return "call返回";
        }
    }
    
    
  • 通过Executor线程池进行创建

3.五种通讯方式

  • 1.管道:速度慢,容量有限,只有父子进程能通讯

  • 2.FIFO:任何进程间都能通讯,但速度慢

  • 3.消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题

  • 4.信号量:不能传递复杂消息,只能用来同步

  • 5.共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存

4.CountDownLatch

堵塞主线程。手动减数。

public class CountDownLatchDemo {
	private static final int THREAD_COUNT_NUM = 7;

	public static void main(String[] args) throws Exception {
		CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT_NUM);
		for (int i = 1; i <= THREAD_COUNT_NUM; i++) {
			int index = i;
			new Thread(() -> {
				try {
					Thread.sleep(Math.abs(new Random().nextInt(5000)));
					System.out.println("第" + index + "颗龙珠已经找到");
				} catch (Exception e) {
					e.printStackTrace();
				}
				 //每收集到一颗龙珠,需要等待的颗数减1
				countDownLatch.countDown();
			}).start();
		}
		 //等待检查,即上述7个线程执行完毕之后,执行await后边的代码
		countDownLatch.await();
		System.out.println("召唤神龙");

        System.out.println("我是主线程,我被阻塞了");
    }
}

5.CyclicBarrier

新建一个线程。不堵塞主线程。

public class CyclicBarrierDemo {
	private static final int THREAD_COUNT_NUM = 7;

	public static void main(String[] args) {
		CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT_NUM, new Runnable() {
			@Override
			public void run() {
				System.out.println("7个法师召集完毕,同时出发,去往不同地方寻找龙珠!");
				// summonDragon();
			}
		});

		for (int i = 1; i <= THREAD_COUNT_NUM; i++) {
			int index = i;
			new Thread(() -> {
				try {
					Thread.sleep(Math.abs(new Random().nextInt(3000)));
					System.out.println("召集第" + index + "个法师");
					barrier.await();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}).start();
			
		}
        System.out.println("我是主线程,我没有被堵塞");
    }
   }

6.ThreadLocal 原理分析,ThreadLocal为什么会出现OOM,出现的深层次原理

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用 ThreadLocal 创建的变量只能被当前线程访问,其他线程则无法访问和修改。简单理解,就是每个线程变量的副本。不相互影响。使用完要手动移除掉。不然会 oom。

普通成员变量是会给多个并发线程共享,有 JMM 问题。可以使用 volatile 使得变量内存可见并且禁止指令重排序。

ThreadLocal 对象归属于没一个线程单独使用。原理是维护一个 ThreadLocalMap 进行 put、set、remove操作。其中 ThreadLocalMap 的 key 是弱引用(gc就会回收如果没有强引用指向它)。key是ThreadLocal自己处理了,但是value就需要我们自己remove。

假设:key 已经被回收了,key = null 但是 value 有个 map 的强引用指向它,所以在 GC 引用链中,它不能被回收,但是 key 不在了,value 也无法使用,value 就变成了占用内存但是又没用的数据。线程如果结束掉,整个 ThreadLocal 会给 GC 掉。只有在常驻的线程里面容易发生内存泄漏问题。

对象存放在哪里?

在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。而堆内存中的对象对所有线程可见

使用场景:

1.Spring 可以定义多个过滤器 Filter,拦截器 HandlerInterceptor ,这些都是带有顺序执行的。多个操作直接需要共享数据如用户登陆信息、请求响应信息等,但是每个请求直接数据又要独立互不干扰。

内存泄漏例子:

public class SimpleThreadLocal {

	private static final int THREAD_LOOP_SIZE = 500;
	private static final int MOCK_DIB_DATA_LOOP_SIZE = 10000;

	private static ThreadLocal<List<User>> threadLocal = new ThreadLocal<>();

	public static void main(String[] args) throws InterruptedException {

		ExecutorService executorService = Executors.newFixedThreadPool(THREAD_LOOP_SIZE);

		for (int i = 0; i < THREAD_LOOP_SIZE; i++) {
			executorService.execute(() -> {
				threadLocal.set(new SimpleThreadLocal().addBigList());
				Thread t = Thread.currentThread();
				System.out.println(Thread.currentThread().getName());
				// threadLocal.remove(); // 不取消注释的话就可能出现OOM
			});
			try {
				Thread.sleep(500L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		// executorService.shutdown();
	}

	private List<User> addBigList() {
		List<User> params = new ArrayList<>(MOCK_DIB_DATA_LOOP_SIZE);
		for (int i = 0; i < MOCK_DIB_DATA_LOOP_SIZE; i++) {
			params.add(new User("xuliugen", "password" + i, "男", i));
		}
		return params;
	}

	class User {
		private String userName;
		private String password;
		private String sex;
		private int age;

		public User(String userName, String password, String sex, int age) {
			this.userName = userName;
			this.password = password;
			this.sex = sex;
			this.age = age;
		}
	}

}

7.线程池的几种实现方式

线程池就是批量先建立好一定数目的线程,使用完后在放回。减少创建销毁需要的资源。

 1)newFixedThreadPool和newSingleThreadExecutor:
  主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
 2)newCachedThreadPool和newScheduledThreadPool:
  主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
// 可以自定义线程池名称
private static final ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(1, new BasicThreadFactory.Builder()
        .namingPattern("redisLock-schedule-pool").daemon(true).build());

线程池的工作原理

  1. 线程池刚创建时,里面没有一个线程。

  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:

    a、如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;

    b、如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。

    c、如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;

    d. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。

  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。

  4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

经过jmeter压力测试亲测:最大线程总数为 maximumPoolSize + 队列 数目。

8.可重用锁

  • 当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁或者其他同步代码块时,如果该锁是重入锁,请求就会成功,否则阻塞。在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的。

9.什么是ABA问题,出现ABA问题JDK是如何解决的

ABA:修改了值,在修改回来。

解决:增加一个版本号,每次修改增加1

10.死锁

两个线程1、2,分别访问两个资源A、B,1线程先请求A在请求B,2线程先请求B再请求A,双方都在等待对方释放资源。造成死锁。

预防:

  • 控制加锁的顺序
  • 使用jdk自带工具jconsole进行死锁检测
  • 使用jstack -l 线程检测

11. join()

等待调用线程执行完毕为止。

上次更新时间: 2024/5/7 05:59:02